/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */

package org.apache.commons.exec.util;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

/**
 * Supplement of commons-lang, the stringSubstitution() was in a simpler
 * implementation available in an older commons-lang implementation.
 *
 * This class is not part of the public API and could change without warning.
 *
 */
public class StringUtils {

	private static final String SINGLE_QUOTE = "\'";
	private static final String DOUBLE_QUOTE = "\"";
	private static final char SLASH_CHAR = '/';
	private static final char BACKSLASH_CHAR = '\\';

	/**
	 * Perform a series of substitutions.
	 * <p>
	 * The substitutions are performed by replacing ${variable} in the target string
	 * with the value of provided by the key "variable" in the provided hash table.
	 * </p>
	 * <p>
	 * A key consists of the following characters:
	 * </p>
	 * <ul>
	 * <li>letter
	 * <li>digit
	 * <li>dot character
	 * <li>hyphen character
	 * <li>plus character
	 * <li>underscore character
	 * </ul>
	 *
	 * @param argStr
	 *            the argument string to be processed
	 * @param vars
	 *            name/value pairs used for substitution
	 * @param isLenient
	 *            ignore a key not found in vars or throw a RuntimeException?
	 * @return String target string with replacements.
	 */
	public static StringBuffer stringSubstitution(final String argStr, final Map<? super String, ?> vars, final boolean isLenient) {

		final StringBuffer argBuf = new StringBuffer();

		if (argStr == null || argStr.isEmpty()) {
			return argBuf;
		}

		if (vars == null || vars.isEmpty()) {
			return argBuf.append(argStr);
		}

		final int argStrLength = argStr.length();

		for (int cIdx = 0; cIdx < argStrLength;) {

			char ch = argStr.charAt(cIdx);
			char del = ' ';

			switch (ch) {

			case '$':
				final StringBuilder nameBuf = new StringBuilder();
				del = argStr.charAt(cIdx + 1);
				if (del == '{') {
					cIdx++;

					for (++cIdx; cIdx < argStr.length(); ++cIdx) {
						ch = argStr.charAt(cIdx);
						if ((ch != '_') && (ch != '.') && (ch != '-') && (ch != '+') && !Character.isLetterOrDigit(ch)) {
							break;
						}
						nameBuf.append(ch);
					}

					if (nameBuf.length() >= 0) {

						String value;
						final Object temp = vars.get(nameBuf.toString());

						if (temp instanceof File) {
							// for a file we have to fix the separator chars to allow
							// cross-platform compatibility
							value = fixFileSeparatorChar(((File) temp).getAbsolutePath());
						} else {
							value = temp != null ? temp.toString() : null;
						}

						if (value != null) {
							argBuf.append(value);
						} else {
							if (!isLenient) {
								// complain that no variable was found
								throw new RuntimeException("No value found for : " + nameBuf);
							}
							// just append the unresolved variable declaration
							argBuf.append("${").append(nameBuf.toString()).append("}");
						}

						del = argStr.charAt(cIdx);

						if (del != '}') {
							throw new RuntimeException("Delimiter not found for : " + nameBuf);
						}
					}

					cIdx++;
				} else {
					argBuf.append(ch);
					++cIdx;
				}

				break;

			default:
				argBuf.append(ch);
				++cIdx;
				break;
			}
		}

		return argBuf;
	}

	/**
	 * Split a string into an array of strings based on a separator.
	 *
	 * @param input
	 *            what to split
	 * @param splitChar
	 *            what to split on
	 * @return the array of strings
	 */
	public static String[] split(final String input, final String splitChar) {
		final StringTokenizer tokens = new StringTokenizer(input, splitChar);
		final List<String> strList = new ArrayList<>();
		while (tokens.hasMoreTokens()) {
			strList.add(tokens.nextToken());
		}
		return strList.toArray(new String[strList.size()]);
	}

	/**
	 * Fixes the file separator char for the target platform using the following
	 * replacement.
	 *
	 * <ul>
	 * <li>'/' &#x2192; File.separatorChar</li>
	 * <li>'\\' &#x2192; File.separatorChar</li>
	 * </ul>
	 *
	 * @param arg
	 *            the argument to fix
	 * @return the transformed argument
	 */
	public static String fixFileSeparatorChar(final String arg) {
		return arg.replace(SLASH_CHAR, File.separatorChar).replace(BACKSLASH_CHAR, File.separatorChar);
	}

	/**
	 * Concatenates an array of string using a separator.
	 *
	 * @param strings
	 *            the strings to concatenate
	 * @param separator
	 *            the separator between two strings
	 * @return the concatenated strings
	 */
	public static String toString(final String[] strings, final String separator) {
		final StringBuilder sb = new StringBuilder();
		for (int i = 0; i < strings.length; i++) {
			if (i > 0) {
				sb.append(separator);
			}
			sb.append(strings[i]);
		}
		return sb.toString();
	}

	/**
	 * Put quotes around the given String if necessary.
	 * <p>
	 * If the argument doesn't include spaces or quotes, return it as is. If it
	 * contains double quotes, use single quotes - else surround the argument by
	 * double quotes.
	 * </p>
	 *
	 * @param argument
	 *            the argument to be quoted
	 * @return the quoted argument
	 * @throws IllegalArgumentException
	 *             If argument contains both types of quotes
	 */
	public static String quoteArgument(final String argument) {

		String cleanedArgument = argument.trim();

		// strip the quotes from both ends
		while (cleanedArgument.startsWith(SINGLE_QUOTE) || cleanedArgument.startsWith(DOUBLE_QUOTE)) {
			cleanedArgument = cleanedArgument.substring(1);
		}

		while (cleanedArgument.endsWith(SINGLE_QUOTE) || cleanedArgument.endsWith(DOUBLE_QUOTE)) {
			cleanedArgument = cleanedArgument.substring(0, cleanedArgument.length() - 1);
		}

		final StringBuilder buf = new StringBuilder();
		if (cleanedArgument.indexOf(DOUBLE_QUOTE) > -1) {
			if (cleanedArgument.indexOf(SINGLE_QUOTE) > -1) {
				throw new IllegalArgumentException("Can't handle single and double quotes in same argument");
			}
			return buf.append(SINGLE_QUOTE).append(cleanedArgument).append(SINGLE_QUOTE).toString();
		}
		if (cleanedArgument.indexOf(SINGLE_QUOTE) > -1 || cleanedArgument.indexOf(" ") > -1) {
			return buf.append(DOUBLE_QUOTE).append(cleanedArgument).append(DOUBLE_QUOTE).toString();
		}
		return cleanedArgument;
	}

	/**
	 * Determines if this is a quoted argument - either single or double quoted.
	 *
	 * @param argument
	 *            the argument to check
	 * @return true when the argument is quoted
	 */
	public static boolean isQuoted(final String argument) {
		return argument.startsWith(SINGLE_QUOTE) && argument.endsWith(SINGLE_QUOTE) || argument.startsWith(DOUBLE_QUOTE) && argument.endsWith(DOUBLE_QUOTE);
	}
}